import { injectable, Service } from './service'
import { invoke } from '@tauri-apps/api/core'
import { appDataDir, join } from '@tauri-apps/api/path'
import { exists, mkdir, readDir, readTextFile, remove, writeTextFile } from '@tauri-apps/plugin-fs'
import { open, save } from '@tauri-apps/plugin-dialog'
import {
  GraphNode,
  GraphEdge
} from '@vue-flow/core'
import { NodeItem } from '@/types/node'
import { ArgumentItem } from '@/types/argument'
import OutputService from './output.service'
import NodeService from './node.service'

type NodeData = NodeItem & {
  inputs: (ArgumentItem & { value: Any })[],
  outputs: (ArgumentItem & { value: Any })[]
}

@injectable
export default class ScriptService extends Service {
  private outputService = new OutputService()
  private nodeService = new NodeService()
  private tmpDir = ''

  constructor () {
    super()
    appDataDir().then(async dir => {
      this.tmpDir = await join(dir, 'tmp')
      if (!await exists(this.tmpDir)) {
        await mkdir(this.tmpDir)
      }
    })
  }

  async run (code: string) {
    // 写临时文件
    code += '\nrun()'
    const _path = await join(this.tmpDir, 'code.py')
    await writeTextFile(_path, code)
    try {
      const res = await invoke<string>('run_code', { code: _path })
      console.log(res)
      return JSON.parse(res)
    } finally {
      remove(_path)
    }
  }

  async compile (nodes: GraphNode<NodeData>[], edges: GraphEdge[]) {
    this.ensureOutputs(nodes)
    const imports = ['import cv2', 'import numpy as np', 'import json']
    const defs = [] as { id: string, script: string }[]
    for (const node of nodes) {
      if (node.type !== 'const' && defs.find(v => v.id === node.data.id)) {
        continue
      }
      defs.push({
        id: node.data.id!,
        script: '# ' + node.label + '\n' + this.createDef(node) + '\n'
      })
    }
    const caches = new Map<string, boolean>()
    const loop = (_node: GraphNode<NodeData>, _actions: string[]): boolean => {
      if (caches.has(_node.id)) {
        return _node.type === 'root'
      }
      let res = false
      // 上层
      for (const edge of edges.filter(v => v.target === _node.id)) {
        const node = nodes.find(v => v.id === edge.source)!
        const _res = loop(node, _actions)
        res = res || _res
        _actions.push(`i_${edge.target}_${edge.targetHandle} = o_${edge.source}_${edge.sourceHandle}`)
      }
      if (_node.type === 'const') {
        _actions.push(
          `${_node.data.outputs.map(v => 'o_' + _node.id + '_' + v.name).join(',')} = ` +
          `fn_const_${_node.id}()`
        )
      } else {
        _actions.push(
          `${_node.data.outputs.map(v => 'o_' + _node.id + '_' + v.name).join(',')} = ` +
          `fn_${_node.data.id}(${_node.data.inputs.map(v => 'i_' + _node.id + '_' + v.name).join(',')}) # 执行 ${_node.label}`
        )
      }
      caches.set(_node.id, true)
      return res || (_node.type === 'root')
    }
    const leafs = nodes.filter(v => edges.findIndex(v1 => v1.source === v.id) < 0)
    for (const node of leafs) {
      const actions = [] as string[]
      if (!loop(node, actions)) {
        continue
      }
      const results = await this.result(nodes, edges)
      return `${imports.join('\n')}\n\n${defs.map(v => v.script).join('\n')}\n` +
        `# 主程序\ndef run():\n\t${actions.join('\n\t')}\n\t# 输出\n\t${results.join('\n\t')}\n\tprint(debug_results)\n`
    }
  }

  async open () {
    const file = await open({
      multiple: false,
      directory: false,
      filters: [
        {
          name: '指令流',
          extensions: ['.sc']
        }
      ]
    })
    if (!file) {
      throw '打开文件失败'
    }
    const txt = await readTextFile(file.path)
    return {
      path: file.path,
      data: JSON.parse(txt)
    }
  }

  async save (data: string, _path?: string) {
    if (!_path) {
      const file = await save({
        title: '选择保存文件',
        filters: [
          {
            name: '指令流',
            extensions: ['.sc']
          }
        ]
      })
      if (!file) {
        return
      }
      _path = file
    }
    await writeTextFile(_path, data)
  }

  private ensureOutputs (nodes: GraphNode<NodeData>[]) {
    for (const node of nodes) {
      if (node.type !== 'root' && node.type !== 'const') {
        continue
      }
      if (node.data.outputs[0].value === undefined) {
        throw `节点: [${node.label}] - 未选择值`
      }
    }
  }

  private async result (nodes: GraphNode<NodeData>[], edges: GraphEdge[]) {
    await this.clearTmpDir()
    const results = [] as string[]
    const outputs = [] as string[]
    for (const node of nodes.filter(v => v.type === 'debug')) {
      const edge = edges.find(v => v.target === node.id)
      const source = nodes.find(v => v.id === edge?.source)
      const output = source?.data.outputs.find(v => v.name === edge?.sourceHandle)
      if (!output) {
        continue
      }
      if (output.type === 'mat') {
        const _path = await join(this.tmpDir, `${node.id}.png`)
        results.push('#保存文件', `cv2.imwrite(r"${_path}", o_${source!.id}_${output.name})`, `r_${node.id} = r"${_path}"`)
      } else {
        results.push(`r_${node.id} = o_${source!.id}_${output.name}`)
      }
      outputs.push(`r_${node.id}`)
    }
    results.push('debug_results = json.dumps({' + outputs.map(v => `'${v}':${v}`).join(', ') + '}, indent=2, sort_keys=True, ensure_ascii=False)')
    return results
  }

  private createDef (node: GraphNode<NodeData>) {
    if (node.type === 'root') {
      return `def fn_root():\n\treturn r"${node.data.outputs[0].value}"`
    } else if (node.type === 'const') {
      let v = node.data.outputs[0].value
      if (node.data.outputs[0].type === 'string') {
        v = '"' + node.data.outputs[0].value + '"'
      } else if (node.data.outputs[0].type === 'bool') {
        v = node.data.outputs[0].value ? 'True' : 'False'
      }
      return `def fn_const_${node.id}():\n\treturn ${v}`
    } else if (node.type === 'debug') {
      return 'def fn_debug(v):\n\treturn v'
    } else {
      const script = this.nodeService.nodes.find(v => v.id === node.data.id)?.script
      if (!script) {
        throw `节点 ${node.data.name} 异常`
      }
      return `def fn_${node.data.id}(${node.data.inputs.map(v => v.name).join(',')}):\n\t${script.split('\n').join('\n\t')}`
    }
  }

  private async clearTmpDir () {
    for (const e of await readDir(this.tmpDir)) {
      await remove(await join(this.tmpDir, e.name))
    }
  }
}
